package org.ricks.log;

import org.ricks.common.conf.LogConfig;
import org.ricks.utils.StrUtil;
import java.util.BitSet;
import java.util.Collection;
import java.util.Map;


public class TinylogLoggingProvider {

	private final TinylogContextProvider context;
	private final Level activeLevel = LogConfig.getLevel();
	private Writer[][] writers;
	private final Collection<LogEntryValue>[] requiredLogEntryValues;
	private final BitSet fullStackTraceRequired;

	public TinylogLoggingProvider() {
		context = new TinylogContextProvider();
		writers = createWriters();
		requiredLogEntryValues = calculateRequiredLogEntryValues(writers);
		fullStackTraceRequired = calculateFullStackTraceRequirements(requiredLogEntryValues);
	}

	public Writer[][] createWriters() {
		if(StrUtil.isNotBlank(LogConfig.getFile())) writers = new Writer[Level.OFF.ordinal()][2]; else writers = new Writer[Level.OFF.ordinal()][1];
		for (int levelIndex = 0; levelIndex < Level.OFF.ordinal(); ++levelIndex) {
			Writer writer = new ConsoleWriter();
			if(writers[levelIndex].length == 1) {
				writers[levelIndex][0] = writer;
			}else {
				writers[levelIndex][0] = writer;
				writers[levelIndex][1] = new RollingFileWriter();
			}
		}
		return writers;
	}

	
	public Collection<LogEntryValue>[] calculateRequiredLogEntryValues(final Writer[][] writers) {
		Collection<LogEntryValue>[] logEntryValues = new Collection[writers.length];
		for (int levelIndex = 0; levelIndex < Level.OFF.ordinal(); ++levelIndex) {
			for (int i = 0; i < writers[levelIndex].length; i++) {
				logEntryValues[levelIndex] = writers[levelIndex][i].getRequiredLogEntryValues();
			}
		}
		return logEntryValues;
	}

	
	public BitSet calculateFullStackTraceRequirements(final Collection<LogEntryValue>[] logEntryValues) {
		BitSet result = new BitSet(logEntryValues.length);
		Collection<LogEntryValue> values = logEntryValues[Level.ERROR.ordinal()];
		if (values.contains(LogEntryValue.METHOD) || values.contains(LogEntryValue.FILE) || values.contains(LogEntryValue.LINE)) {
			result.set(Level.ERROR.ordinal());
		}
		return result;
	}

	public void log(Thread currThread,StackTraceElement stackTraceElement, final boolean base, final Level level, final Throwable exception, final AdvancedMessageFormatter formatter,
                    final Object obj, final Object... arguments) {
		if (activeLevel.ordinal() <= level.ordinal()) {
			LogEntry logEntry = createLogEntry(currThread,stackTraceElement, base, level, exception, formatter,
					obj, arguments, requiredLogEntryValues, context);
			output(logEntry, writers[logEntry.getLevel().ordinal()]);
		}
	}
	
	private void output(final LogEntry logEntry, final Writer[] writers) {
		try {
			if (writers == null || logEntry == null) {
				close(writers);
				return;
			}
			for (int i = 0; i < writers.length; i++) {
				writers[i].write(logEntry);
				writers[i].flush();
			}
		}catch (Exception ex) {
				InternalLogger.log(Level.ERROR, ex, "Failed to write log entry ");
		}
	}


	/**
	 * Flushes a collection of writers.
	 *
	 *            Writers to flush
	 */
	private void flush(final Writer writer) {
		try {
			writer.flush();
		} catch (Exception ex) {
			InternalLogger.log(Level.ERROR, ex, "Failed to flush writer");
		}
	}

	/**
	 * Closes all writers.
	 */
	private void close(Writer[] writers) {
		try {
			for (int i = 0; i < writers.length; i++) {
				writers[i].close();
			}
		} catch (Exception ex) {
			InternalLogger.log(Level.ERROR, ex, "Failed to close writer");
		}
	}


	public static LogEntry createLogEntry(Thread currThread,final StackTraceElement stackTraceElement, final boolean base,
                                                       final Level level, final Throwable exception, final AdvancedMessageFormatter formatter, final Object obj,
                                                       final Object[] arguments, final Collection<LogEntryValue>[] requiredLogEntryValues,
                                                       final TinylogContextProvider contextProvider) {
		Collection<LogEntryValue> required = requiredLogEntryValues[level.ordinal()];

		Timestamp timestamp = RuntimeProvider.createTimestamp();
		Thread thread = required.contains(LogEntryValue.THREAD) ? currThread : null;
		Map<String, String> context = required.contains(LogEntryValue.CONTEXT) ? contextProvider.getMapping() : null;

		String className;
		String methodName;
		String fileName;
		int lineNumber = -1;
		if (stackTraceElement == null) {
			className = null;
			methodName = null;
			fileName = null;
		} else {
			className = stackTraceElement.getClassName();
			methodName = stackTraceElement.getMethodName();
			fileName = stackTraceElement.getFileName();
			lineNumber = stackTraceElement.getLineNumber();
		}

		String message;
		if (arguments == null || arguments.length == 0) message = obj == null ? null : obj.toString(); else message = formatter.format((String) obj, arguments);
		return new LogEntry(timestamp, thread, context, className, methodName, fileName, lineNumber, base, level, message, exception);
	}
}
